背景介绍
最近开发的几个工程使用的都是系统的VCStack,即UITabbarController
+ UINavigationController
的方式。这是一个经典的组合,在现实的开发场景中基本已经能够满足需求。但是,最近几期UI稿和UE稿的设计规则,有点超出了这个既有框架的能力
- 遮罩全屏的半浮层
- 出栈入栈复杂的动画
- 跨VC堆栈的
pop
和push
操作
在现有的UITabbarController
+ UINavigationController
结构下,这些功能已经被实现,但是过程较为复杂,不少逻辑现在看来任有优化的空间,基于这个背景,打算写一个自定义的VCStack,解决系统空间的局限性
系统VCStack存在的困境
在构思自定义VCStack之前,回顾了一下系统控件在日常开发中存在的瓶颈,这些瓶颈在日查那个的业务开发中经常困扰着我们,拖累开发人员的效率。总结了一下,有以下几点:
UI***Bar层级过高导致的页面遮挡问题
出/入栈动画支持不够友好的问题
1
2* 任意时间点getTopVC带来的问题
```堆栈的操作往往伴随着动画,动画中包含时间,如果我们在不合适的时间节点getTopVC可能导致之后的UI操作完全失效。比如,view正在消失的时候获取topVC并在vc.view中增加UI的处理布局标准的问题。
1
2* 交叉影响。
```这里举一个例子:修改Navigation的backItem会导致系统默认的优化手势失效,需要复写此功能才能生效指定堆栈的跳转。
1
2* 模态视图继续跳转的问题
```这是一种经常出现的场景,模态一个视图,在这个模态视图的基础上还存在堆栈的操作。当前的实现大多是在模态的基础上再包一层NavigationController,让其具备堆栈操作的能力
上面几个case使我们自定义VCStack解决的核心问题,本文也会按照这几个痛点展开讲解是如何一一解决这些问题的
自定义VCStack是什么
先交代一下这个VCStack到底是什么,系统NavigationController的效果我们都不陌生,如何在不继承系统NavigationController的基础上实现一套自己的VCStack管理机制呢(保持效果一致的原则)?从日常的使用中,我们了解到系统的NavigationController其实一个堆栈管理器,之中最重要的是VC的管理,可能是顶层封装的原因使得我们对整个管理体系了解不多。但是有几点是可以猜测到的
1、所有的VC都拥有自己的View
2、所有的View都是在根Window上展示的
3、你看到的动画只是管理器让交互不再生硬做出的表象
意识到这三点,接下来就好办了,VC是独立的,可以在任意节点创建和销毁,我们的VCStack只需要管理他们的显示逻辑和已有的生命周期。所以VCStack只要找到切合的时间点叠加和管理这些VC即可。首先有个统一的入口
1 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { |
这个节点中window需要一个rootViewController,这是VCStack接入的切口,一个VC创建并作为RootViewController被VCStack持有,VCStackInstance.rootViewController作为参数给到Window。这一步操作已经为VCStack打下了基石,因为之后所有VC.view的叠加都有了rootView.接下来的事情就变的简单了
1、push操作将vc.view叠加到currentVC
2、pop操作将vc.view从上一个vc.view移除
这期间需要兼顾的东西还有很多,比如
1、vc生命周期的一致
2、手势操作
3、动画接入
对整个想做的事情有了一定的了解了之后,下面是一些实现中的细节
逐个击破
视图层级 + 布局原点
自定义VCStack不会再有TopLayout和BottomLayout这种预置依赖,所有的View的布局都将从window的(0,0)点开始布局。navigationBar
和TabBar
也将会被CustomView代替以此抹平层级间Z轴差距过大导致的遮罩问题
[图片上传中…(系统navigation层级.png-6d0e8b-1545878789170-0)]
当Window的整个区域都有权限去管理之后,层级和布局原点的问题就已经不是问题了,但是这样又引入了其他问题:
- 自定义navigationBar增加了每个页面开发的成本
- 自定义TabBar增加了每个页面开发的成本
一个好的方法就是创建一个快捷的模板类,将常用的NavigationBar和常用的TabBar封装成模板输出,增加开发效率
1 | @interface UIViewController (NavigationBar) |
动画拓展性
系统的Navigation堆栈的跳转提供的api并不多
1 | - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated; // Uses a horizontal slide transition. Has no effect if the view controller is already in the stack |
跳转中动画的支持方式为Bool值,这就限定了跳转中的动画拓展性。当然,设计系统的人为了能让跳转中的动画得到更高粒度的支持,实现了NavigationControllerDelegate这套协议,在集成了这套协议的VC中,可以将动画拓展的更好,协议如下:
1 | - (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController |
但是任然有缺陷,细想一下,这样的协议是在哪个层面实现呢?
1、直接耦合到需要动画支持的VC?
2、抽象到UIViewController层面的统一代理?
1的方式在实际的使用中,算是较多的一种,但是存在拓展性和逻辑抽象的问题,相同的问题在另一个场景下,大多的复用方式是:copy + 粘贴。场景少还能理解,一旦这样场景多了,这种方式带来的问题就会凸显出来。渐渐的在使用系统VCStack的基调下,就会有人抽象这个层面的信息,做一个统一的管理,形成了2的这种方式,但是,2这种方式也是存在问题的,先看一下抽象层面的信息:
- currentVC
- willShowVC
- operation
关键点出在了operation,这是系统的枚举类型,和业务场景中的契合度不是很高,限制了动画的类型。这相当于找到了这个动画支持的痛点,现在讲一下我的思路:
在自定义的VCStack中将动画完全交出去,以实例的形式交出去,这看起来有点难以理解。如何统一实例的api?这就用到了协议。所有的animation实例是继承AnimationProtocol的,由这个协议来约束api,使得所有实例的调度一致。结构如下:
下面是实例的生成api,在实际的使用中每个独具特色的动画协议都是这么写的,他们的具体实现放在了集成的协议中
1 | @interface HDVCStackAnimation : NSObject <HDVCStackAnimationProtocol> |
协议本身和堆栈的逻辑保持一致
1 | @protocol HDVCStackAnimationProtocol <NSObject> |
协议的实现也是面向切面的,只需要关注当前的参数和逻辑,例如如下是一个模拟系统自带的堆栈动画的协议实现
1 | @implementation HDVCStackAnimation |
调用API的简化:
1 | [self.vcStack pushto:vc animation:[HDVCStackAnimation defaultAnimation]]; |
可以看到,优化之后的动画api参数也是三个
- currentVC
- willShowVC
- AnimationInstance
但是这里的animationInstance实现的空间大大增加,他只要继承自AnimationProtocol,具体的animation如何实现已经完全交给了业务层。如果在业务层的设计上适配几套符合当前场景的animation,这样的抽象也会被简化到为数不多的Animation实例中。满足了我们的要求,拓展性和逻辑抽象
getTopVC + 交叉影响
在完全接手了VCStack之后,对于操作的每个细节都在开发者的掌握之中,当任务触达的时候,可以追加AnimationCompletionHandle的处理,来让这个逻辑更加健壮。同样的交叉影响的存在也被开发人员决定,只有设计中存在这种交叉影响,才会在使用中存在这样的逻辑。设计的节点已经被开发人员管控,需不需要这种逻辑交互已经不再是一个黑盒
指定VC的跳转
这个功能在实际的业务中会经常遇到,在系统Navigation的基础上的实现如下
1、遍历navigationController.viewControllers
2、找到匹配的VC实例
3、执行popToVC操作
前面两步基本不可避免,导致在实际的落地式往往一堆一堆代码的存在,对于代码简洁来说不是一个很好的方案。考虑到这样的需求场景,VCStack中集成了一套快捷的跳转API,覆盖了常见的业务场景
1 | /** |
逻辑的处理已经在VCStack内部完成,只需要简单的API调用就可以完成业务需求
模态视图后续堆栈跳转
如果在模态视图中还存在堆栈的跳转,系统VCStack基础下的处理基本是在modalVC上包装一层VCStack,使其具备这样的能力,但是这里会存在问题,两个navigationStack的间接断开,如果这里执行popToVC会带了大量的逻辑判断。使用了自定义VCStack可以将modal视图的出现规划到push操作中,只是这里的动画实例发生了改变
1 | @implementation HDModelAnimation |
这样的操作和模态视图出现和消失的视觉效果等效,同时保持了VCStack链
1 | [self.vcStack pushto:vc animation:[HDModelAnimation defaultAnimation]]; |
细节
在自定义VCStack中设计到很多细节操作,这些操作的完善会让整个VCStack更加的健壮
生命周期维护
在VCStack中除了view的依赖的管理,同步操作还需要将对应的VC的生命周期管理起来,在日常的业务场景中这几个生命周期使用的频次是最高的
- viewWillAppear
- viewDidAppear
- viewWillDisappear
- viewDidDisappear
- dealloc
为了保持和系统生命周期的一致性,在push和pop操作中对VC的生命周期做了手动处理
1 | - (void)pushto:(UIViewController *)vc animation:(NSObject<HDVCStackAnimationProtocol> *)animation { |
对于dealloc 在持有链消失的时候能被系统检测到,可以正常的释放,当前的持有关系为:
- VCStack持有数组
- 数组持有VC
- vc弱持有VCStack
其中VC弱持有VCStack是为了兼容tabBarController的存在,如果工程是一个单一的VCStack完全可以用单例待提升实例。在pop的时候会主动解开所有的依赖
1 | VC.vcStack = nil |
手势系统维护
在每次push的时候,都会在View的层级上增加手势系统,当然这里也有协议的支持,如果VC实现了协议
1 | @protocol HDVCEnableDragBackProtocol <NSObject> |
并标记为NO的时候,这个页面是不支持手势的。具体实现如下:
1 | - (void)pushto:(UIViewController *)vc animation:(NSObject<HDVCStackAnimationProtocol> *)animation { |
动画期间手势隔离
自定义VCStack提供了很多便捷的操作API,这些api中很多是伴有animation 操作的,为了避免用户在animation期间响应手势导致一些未知的错误,在代码段做了容错
1 | - (void)pushto:(UIViewController *)vc animation:(NSObject<HDVCStackAnimationProtocol> *)animation { |
总结
在实现的过程中,一开始的实现是围绕着一个NavigationStack的方式去进行的,这在实际的开发中已经满足了大多需求,因为大多的app都是一个Navigation的方式管理的,即便底部存在多个业务窗口,但是在下一级页面都会关闭底部的这个入口。
为了支持系统tabBar和VCStack混合管理的方式,在原来的基础上集成了tabBarManager+VCStack。是的整体的逻辑更靠近系统TabBar+navigation的管理方式。
最后说一句项目还在完善中,如果有兴趣可以一并完善。项目地址如下
VCStack
VCStack+TabBarManager